define(
    [ "jquery", "domReady!" ],
    function($) {
      var DOUBLE_SUBMIT_BLOCK_KEY = 'blockDoubleSubmit_submitPressed';

      // Official shim from
      // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/map
      // Production steps of ECMA-262, Edition 5, 15.4.4.19
      // Reference: http://es5.github.com/#x15.4.4.19
      if (!Array.prototype.map) {
        Array.prototype.map = function(callback, thisArg) {
          var T, A, k;
          if (this == null) {
            throw new TypeError(" this is null or not defined");
          }
          var O = Object(this);
          var len = O.length >>> 0;
          if ({}.toString.call(callback) != "[object Function]") {
            throw new TypeError(callback + " is not a function");
          }
          if (thisArg) {
            T = thisArg;
          }
          A = new Array(len);
          k = 0;
          while (k < len) {
            var kValue, mappedValue;
            if (k in O) {
              kValue = O[k];
              mappedValue = callback.call(T, kValue, k, O);
              A[k] = mappedValue;
            }
            k++;
          }
          return A;
        };
      }

      // Shim from
      // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/reduce
      if (!Array.prototype.reduce) {
        Array.prototype.reduce = function reduce(accumulator) {
          if (this === null || this === undefined)
            throw new TypeError("Object is null or undefined");
          var i = 0, l = this.length >> 0, curr;

          if (typeof accumulator !== "function") // ES5 : "If
            // IsCallable(callbackfn) is
            // false, throw a TypeError
            // exception."
            throw new TypeError("First argument is not callable");
          if (arguments.length < 2) {
            if (l === 0)
              throw new TypeError("Array length is 0 and no second argument");
            curr = this[0];
            i = 1; // start accumulating at the second element
          } else
            curr = arguments[1];
          while (i < l) {
            if (i in this)
              curr = accumulator.call(undefined, curr, this[i], i, this);
            ++i;
          }
          return curr;
        };
      }

      if (!Array.prototype.filter) {
        Array.prototype.filter = function filter(f) {
          return Array.prototype.reduce(function(prev, cur) {
            if (f(cur))
              prev.push(cur);
            return prev;
          }, new Array());
        };
      }

      var me = {};
      me.FADE_SPEED = 300;

      /*
       * Calls hook function when the user clicks *outside* of the given object.
       * 
       * jObj: jQuery object or string hook: the function to call when clicks
       * happen partners: an optional list of elements clicks on which should
       * not invoke the hook
       */
      me.addClickOut = function(jObj, hook, partners) {
        jObj = $(jObj);

        function isTarget(event, jObj) {
          return $(event.target).parents().andSelf().is(jObj);
        }

        /* Global document click handler */
        $(document).click(function(event) {
          if (isTarget(event, jObj) || !jObj.is(":visible")) {
            return;
          } else if (partners != null) {
            var matched = false;

            $(partners).each(function() {
              if (isTarget(event, $(this))) {
                matched = true;
                return false;
              }
            });

            if (matched) {
              return;
            }
          }

          hook();
        });
      };

      /*
       * Clear error messages when a user starts typing into a field. In
       * particular, this removes the class "error" from the field (added by
       * stripes), and fades out the next direct sibling element with the class
       * .error-status.
       *
       * jObj: jQuery object or string
       */
      me.addErrorClearer = function(jObj) {
        jObj = $(jObj);
        var deerrored = false;
        var errmsg = jObj.nextAll(".error-status").first();
        var deErrorHandler = function(e) {
          if (deerrored === false) {
            jObj.removeClass("error");
            errmsg.fadeOut(me.FADE_SPEED, function() {
              if (window.parent && window.parent.feedbackErrorRemoveCallback) {
                window.parent.feedbackErrorRemoveCallback();
              } else if (window.feedbackErrorRemoveCallback) {
                window.feedbackErrorRemoveCallback();
              }
            });
            deerrored = true;
          }
        };
        if (jObj.is("select") || jObj.is(":checkbox")) {
          jObj.bind("change", deErrorHandler);
        } else {
          jObj.bind("keyup", deErrorHandler);
        }
      };

      /*
       * Clear error messages below the given field field. Also removes the
       * class "error" from the given field.
       *
       * onField: the input field with the associated error
       * errorDiv: the error div below onField containing the error messages
       */
      me.clearError = function(onField, errorDiv) {
        var errors = errorDiv.children(".error-status");
        errors.remove();
        onField.removeClass('error');
      };

      /*
       * Adds an error message below a given field. Also adds the class
       * "error" to the given field.
       * 
       * onField: the input containing erroneous input
       * errorDiv: the error div below onField that should receive the error message
       * message: the text of the error to add
       */
      me.setError = function(onField, errorDiv, message) {
        var errors = errorDiv.children(".error-status");
        if (errors.size() > 0) {
          errors.html(message);
        } else {
          errorDiv.html("<div class=\"error-status\">" + message + "</div>");
        }
        onField.addClass('error');
      };

      /*
       * Stop an event (such as submit or click) from happening multiple times
       */
      var blockDoubleEvent = function(jObj, eventName) {
        jObj = $(jObj);
        jObj.on(eventName, function() {
          if ($(this).data(DOUBLE_SUBMIT_BLOCK_KEY)) {
            return false;
          }
          $(this).data(DOUBLE_SUBMIT_BLOCK_KEY, true);
        });
      }

      /*
       * Block against submitting a form twice with two successive clicks
       * formObj: form DOM object or jQuery object
       */
      me.blockDoubleSubmit = function(formObj) {
        blockDoubleEvent(formObj, 'submit.blockDoubleSubmit');
      };

      /*
       * Block against clicking a link twice
       */
      me.blockDoubleClick = function(jObj) {
        blockDoubleEvent(jObj, 'click');
      }

      /*
       * Center element on the screen via absolute positioning
       * 
       * jObj: jQuery object or string keepCentered: true/false for whether we
       * should register a resize handler
       */
      me.center = function(jObj, keepCentered) {
        jObj = $(jObj);

        if (keepCentered == null) {
          keepCentered = false;
        }

        if (keepCentered) {
          $(window).resize(function() {
            me.center(jObj, false);
          });
        }

        jObj.css("position", "absolute");
        jObj.css("left", (($(window).width() - jObj.outerWidth()) / 2)
            + $(window).scrollLeft() + "px");

        /* We change behaviour when we run out of space */
        if (jObj.height() >= $(window).height()) {
          jObj.css("top", "0px");
          /* A necessary hack to ensure scrolling is possible */
          $("body").css("min-height", jObj.height() + "px");
        } else {
          jObj.css("top", (($(window).height() - jObj.outerHeight()) / 2)
              + $(window).scrollTop() + "px");
        }

        return jObj;
      };

      me.loadCss = function(url) {
        var link = document.createElement("link");
        link.type = "text/css";
        link.rel = "stylesheet";
        link.href = url;
        document.getElementsByTagName("head")[0].appendChild(link);
      };

      me.addPlaceholderText = function(textArea, msg, ignoreBlurReplace) {

        // Adapted from
        // http://robertnyman.com/2010/06/17/adding-html5-placeholder-attribute-support-through-progressive-enhancement/

        // Detect support for HTML placeholder attribute
        var fakeInput = document.createElement("input"), placeHolderSupport = ("placeholder" in fakeInput);

        if (placeHolderSupport) {
          textArea.attr("placeholder", msg);
        } else {
          // Fallback implementation for browsers that don't support placeholder
          // natively
          var clearValue = function() {
            if (textArea.val() === msg) {
              textArea.val("");
            }
          };
          var setValue = function() {
            if (textArea.val().length === 0) {
              textArea.val(msg);
              textArea.addClass("placeholder-input");
            }
          };

          setValue();

          textArea.bind("focus", function() {
            textArea.removeClass("placeholder-input");
            clearValue();
          });

          if (!ignoreBlurReplace) {
            textArea.bind("blur", setValue);
          }

          // Clear the placeholder text on form submit
          textArea.parents("form").bind("submit", function() {
            clearValue();
          });

          // Clear on reload to prevent the browser from storing the placeholder
          // for autocomplete
          $(window).bind("unload", function() {
            clearValue();
          });
        }
      };
      
      // (IE8 bug) force a DOM re-evaluation and redraw
      me.forceContainerRefresh = function(container) {
        container.each(function() {
          this.className = this.className;
        });
      };
      
      var escapeMap = {
          "&": "&amp;",
          "<": "&lt;",
          ">": "&gt;",
          '"': '&quot;',
          "'": '&#39;'
        };

        me.escapeHTML = function(string) {
          return String(string).replace(/&(?!\w+;)|[<>"']/g, function (s) {
            return escapeMap[s] || s;
          });
        }

      me.settingsTabToggle = function() {
        var tabs = $('.tabs');
        var ul = $('ul', tabs);
        var button = $('#double-brand');
        $('#double-brand').click(function() {
          if (tabs.css('display') == "block") {
            if (ul.hasClass('closed')) {
              button.addClass('active');
              ul.slideDown(function() {
                ul.removeClass('closed');
                ul.css('display','');
              });
            } else {
              button.removeClass('active');
              ul.slideUp(function() {
                ul.addClass('closed');
                ul.css('display','');
                ul.css('height', '');
              });
            }
          }
        });
      };

      me.disableSelectOptions = function(selectElem) {
        selectElem = $(selectElem);
        var selectDom = selectElem.get(0);
        $('option[disabled]', selectElem).css({'color': '#cccccc'});
        selectElem.change(function() {
          if (this.selectedIndex == -1) {
            do {
              this.selectedIndex++;
              if (this.selectedIndex == this.options.length) {
                this.selectedIndex = 0;
                return;
              }
            } while (this.options[this.selectedIndex].disabled);
          }
          if (this.options[this.selectedIndex].disabled) {
            if (this.options.length == 0) {
              this.selectedIndex = -1;
            } else {
              this.selectedIndex--;
            }
            $(this).trigger('change');
          }
        });
        if (selectDom.selectedIndex >= 0 &&
            selectDom.options[selectDom.selectedIndex].disabled) {
          selectDom.onchange();
        }
      };

      // detect IE
      me.isIE7 = $('html.ie7').length
        || $('html.ie7', window.parent.document).length;
      me.isIE = me.isIE7
        || $('html.ie8').length
        || $('html.ie8', window.parent.document).length
        || $('html.ie9').length
        || $('html.ie9', window.parent.document).length;

      /*
       * Javascript workaround to center an element for browsers
       * where `margin: auto` does not work (e.g. IE7).
       */
      me.centerElement = function(element) {
        element = $(element);
        element.css({
          top: '50%',
          left: '50%',
          'margin-top': element.height() / -2,
          'margin-left': element.width() / -2
        });
      };

      return me;
    });
